/**
 * Convert - Type Conversion Routines
 *
 * @author Chris Pratt
 *
 * 7/23/1999
 */
package com.anodyzed.onyx.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Convert {
  private static final String DATE_REGEX = "^((?:[01]?\\d)|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)|(?:January|February|March|April|June|July|August|September|October|November|December))(?:[ \\-/.]((?:(?:[0-2]?\\d)|(?:3[01]))),?)?[ \\-/]((?:[12]\\d)?\\d\\d)$";
  private static final String ISO_REGEX  = "^([12]\\d\\d\\d)[ \\-/.](\\d\\d)[ \\-/.](\\d\\d)$";

  private static final Set<String> TRUISMS = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
  private static final Set<String> FALSEHOODS = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
  static {
    TRUISMS.add("true");
    TRUISMS.add("yes");
    TRUISMS.add("on");
    TRUISMS.add("t");
    TRUISMS.add("y");
    TRUISMS.add("1");
    FALSEHOODS.add("false");
    FALSEHOODS.add("no");
    FALSEHOODS.add("off");
    FALSEHOODS.add("f");
    FALSEHOODS.add("n");
    FALSEHOODS.add("0");
  }

  /**
   * Protected Constructor - Prevent Instantiation of this Utility Singleton
   */
  protected Convert () {
  } //Convert

  ////////// Simple Type Conversion //////////

  /**
   * Convert the String to a boolean
   *
   * @param str the string
   * @return true if the string was "true" | "yes" | "on" | "y" | "1"
   */
  public static boolean toBoolean (String str) {
    return (str != null) && TRUISMS.contains(str);
  } //toBoolean

  /**
   * Convert the String to a boolean, return a boolean if the value can't be 
   * determined.
   *
   * @param str the String
   * @param def The default value
   * @return true if str was "true" | "yes" | "on" | "t" | "y" | "1"
   *         false if str was "false" | "no" | "off" | "f" | "n" | "0"
   *         otherwise {@code def}
   */
  public static boolean toBoolean (String str,boolean def) {
    if(str != null) {
      if(TRUISMS.contains(str)) {
        return true;
      } else if(FALSEHOODS.contains(str)) {
        return false;
      }
    }
    return def;
  } //toBoolean

  /**
   * Convert the String to a Boolean, return the supplied default Boolean 
   * value if the value can't be determined from the supplied string.
   *
   * @param str the String 
   * @param def The default value 
   * @return TRUE if str was "true" | "yes" | "on" | "t" | "y" | "1"
   *         FALSE if str was "false" | "no" | "off" | "f" | "n" | "0"
   *         otherwise {@code def}
   */
  public static Boolean toBoolean (String str,Boolean def) {
    if(str != null) {
      if(TRUISMS.contains(str)) {
        return Boolean.TRUE;
      } else if(FALSEHOODS.contains(str)) {
        return Boolean.FALSE;
      }
    }
    return def;
  } //toBoolean

  /**
   * Convert the String to an Integer or return the default value if it can't
   *  be converted.
   *
   * @param str The String to be converted
   * @param def The default value
   * @return The Converted Integer
   */
  public static int toInt (String str,int def) {
    if(str != null) {
      try {
        return Integer.parseInt(str);
      } catch(NumberFormatException x) {
        // Just return the default value
      }
    }
    return def;
  } //toInt

  /**
   * Convert the String to a Long Integer or return the default value if it
   *  can't be converted.
   *
   * @param str The String to be converted
   * @param def The default value
   * @return The Converted Long Integer
   */
  public static long toLong (String str,long def) {
    if(str != null) {
      try {
        return Long.parseLong(str);
      } catch(NumberFormatException x) {
        // Just return the default value
      }
    }
    return def;
  } //toLong

  /**
   * Convert the String to a float or return the default value if it can't
   *  be converted.
   *
   * @param str The String to be converted
   * @param def The default value
   * @return The Converted float
   */
  public static float toFloat (String str,float def) {
    if(str != null) {
      try {
        return Float.parseFloat(str);
      } catch(NumberFormatException x) {
        // Just return the default value
      }
    }
    return def;
  } //toFloat

  /**
   * Convert the String to a double or return the default value if it can't
   *  be converted.
   *
   * @param str The String to be converted
   * @param def The default value
   * @return The Converted double
   */
  public static double toDouble (String str,double def) {
    if(str != null) {
      try {
        return Double.parseDouble(str);
      } catch(NumberFormatException x) {
        // Just return the default value
      }
    }
    return def;
  } //toDouble

  /**
   * Convert an int to a String using the specified base and the minimum
   *  number of digits.
   *
   * @param i The Integer to Convert
   * @param dig The Number of digits (-1 = any)
   * @param base The base numbering system
   * @return The String
   */
  public static String toString (int i,int dig,int base) {
    StringBuilder buf = new StringBuilder();
    boolean neg = i < 0;
    if(neg) {
      i *= -1;
    }
    while(i > 0) {
      buf.append(Hex.hex(i % base));
      i /= base;
      --dig;
    }
    while(dig-- > 0) {
      buf.append('0');
    }
    if(neg) {
      buf.append('-');
    }
    return buf.reverse().toString();
  } //toString

  /**
   * Convert an int to a String of Decimal Character with a minimum number
   *  of digits.
   *
   * @param i The Integer to Convert
   * @param dig The Minimum number of digits
   * @return The String
   */
  public static String toString (int i,int dig) {
    return toString(i,dig,10);
  } //toString

  /**
   * Convert an int to String of Decimal Characters
   *
   * @param i The Integer to Convert
   * @return The String
   */
  public static String toString (int i) {
    return toString(i,-1,10);
  } //toString

  ////////// Collection Conversion //////////

  /**
   * Add the elements of a delimited string to a Collection.
   *
   * @param str The delimited String
   * @param delim The delimiter Character
   * @param col The Collection to add the elements to
   * @return The Collection object
   */
  public static <T extends Collection<String>> T toCollection (String str,char delim,T col) {
    if((str != null) && (str.length() > 0)) {
      int i = 0;
      char[] buf = str.toCharArray();
      StringBuilder cell = new StringBuilder();
      do {
        while((i < buf.length) && (buf[i] <= ' ')) {
          ++i;
        }
        if((i < buf.length) && (buf[i] == '\"')) {
          i++;
          while(true) {
            if((i < buf.length) && (buf[i] != '\"')) {
              cell.append(buf[i++]);
            } else if((buf.length > i + 1) && (buf[i + 1] == '\"')) {
              cell.append(buf[i++]);
              ++i;
            } else {
              break;
            }
          }
          while((i < buf.length) && (buf[i] != delim)) {
            ++i;
          }
          col.add(cell.toString());
        } else {
          while((i < buf.length) && (buf[i] != delim)) {
            cell.append(buf[i++]);
          }
          col.add(cell.toString().trim());
        }
        cell.setLength(0);
      } while(++i < buf.length);
        // Handle the empty cell at the end of the string
      if(buf[buf.length - 1] == delim) {
        col.add("");
      }
    }
    return col;
  } //toCollection

  /**
   * Convert a delimited String to a List Object.
   *
   * @param str The delimited String
   * @param delim The Delimiter character
   * @return The List Object
   */
  public static List<String> toList (String str,char delim) {
    return toCollection(str,delim,new ArrayList<String>());
  } //toList

  ////////// Array Conversion //////////

  /**
   * Convert a (Possibly Quoted) delimited string of arguments to an array of 
   * strings.
   *
   * @param str The Input string
   * @param delim The delimiter character
   * @return The Array of Strings
   */
  public static String[] toStringArray (String str,char delim) {
    if(str != null) {
      return toList(str,delim).toArray(new String[0]);
    }
    return new String[0];
  } //toStringArray

  ////////// Delimited String Conversion //////////

  /**
   * Convert an Object to be a delimited cell String.
   *
   * @param o Object to be Converted
   * @return The cell String
   */
  protected static String toCell (Object o) {
    boolean quote = false;
    String str = String.valueOf(o);
    StringBuilder cell = new StringBuilder();
    if((str.indexOf(',') != -1) || (str.indexOf('\"') != -1)) {
      quote = true;
      cell.append('\"');
    }
    int prev = 0,pos = 0;
    while((pos = str.indexOf('\"',pos)) != -1) {
      cell.append(str.substring(prev,pos)).append('\"');
      prev = pos++;
    }
    if(prev < str.length()) {
      cell.append(str.substring(prev));
    }
    if(quote) {
      cell.append('\"');
    }
    return cell.toString();
  } //toCell

  /**
   * Convert an Iterable Collection to a delimited String.
   *
   * @param i The Iterable Collection to be Converted
   * @param delim The delimiter character
   * @return The delimited string
   */
  public static String toDelimitedString (Iterable<?> i,char delim) {
    boolean comma = false;
    StringBuilder buf = new StringBuilder();
    for(Object o : i) {
      if(comma) {
        buf.append(delim);
      } else {
        comma = true;
      }
      buf.append(toCell(o));
    }
    return buf.toString();
  } //toDelimitedString

  /**
   * Return a delimited string of key=value pairs for each entry in the map.
   *
   * @param map The map to be converted
   * @param delim The delimiter character
   * @return the delimited key=value string
   */
  public static String toDelimitedString (Map<?,?> map,char delim) {
    StringBuilder buf = new StringBuilder();
    if((map != null) && !map.isEmpty()) {
      boolean comma = false;
      for(Object key : map.keySet()) {
        if(comma) {
          buf.append(delim);
        } else {
          comma = true;
        }
        buf.append(toCell(String.valueOf(key) + '=' + String.valueOf(map.get(key))));
      }
    }
    return buf.toString();
  } //toDelimitedString

  /**
   * Convert an Array of Objects to a single delimited String.
   *
   * @param array The Array of Objects
   * @param delim The delimiter character
   * @return The delimited string
   */
  public static String toDelimitedString (Object[] array,char delim) {
    if(array != null) {
      boolean comma = false;
      StringBuilder buf = new StringBuilder();
      for(Object o : array) {
        if(comma) {
          buf.append(delim);
        } else {
          comma = true;
        }
        buf.append(toCell(o));
      }
      return buf.toString();
    }
    return null;
  } //toDelimitedString

  ////////// Fixed-Format String Conversion //////////

  /**
   * Convert a List of Objects to a fixed width String. 
   *  
   * Each entry in the {@code len} array determines the width of the 
   * corresponding column. If the length of the data for the specified 
   * column is greater then the absolute value of the width in {@code len} it 
   * will be truncated to fit.  If the data is shorter and the width is 
   * positive (i.e. > 0) the data will be Left Justified and padded on the 
   * right end with spaces. If the width is negative (i.e. < 0) the column 
   * will be Right Justified and padded on the Left with zeroes.
   *
   * @param list The List of Objects
   * @param len The Array of Column Widths
   * @return The Fixed Width String
   */
  public static String toFixedString (Iterable<?> list,int[] len) {
    int n,l,i = 0;
    char ch;
    String str;
    StringBuilder buf = new StringBuilder();
    for(Object o : list) {
      n = 0;
      l = Math.abs(len[i]);
      if(o != null) {
        str = String.valueOf(o);
        if((n = str.length()) > l) {
          buf.append(str.substring(0,l));
        } else {
          if(len[i] < 0) {
            while(n++ < l) {
              buf.append('0');
            }
          }
          buf.append(str);
          while(n++ < l) {
            buf.append(' ');
          }
        }
      } else {
        ch = (len[i] < 0) ? '0' : ' ';
        while(n++ < l) {
          buf.append(ch);
        }
      }
      ++i;
    }
    while(i++ < len.length) {
      l = Math.abs(len[i]);
      ch = (len[i] < 0) ? '0' : ' ';
      for(n = 0;n < l;n++) {
        buf.append(ch);
      }
    }
    return buf.toString();
  } //toFixedString

  /**
   * Convert an Array of Objects to a fixed width String.  Each entry in the
   *  <code>len</code> array determines the width of the corresponding column.
   *  If the length of the data for the specified column is greater than the
   *  absolute value of the width in <code>len</code> it will be truncated to
   *  fit.  If the data is shorter and the width is positive (i.e. > 0) the
   *  data will be Left Justified and padded on the right end with spaces. If
   *  the width is negative (i.e. < 0) the column will be Right Justified and
   *  padded on the Left with zeroes.
   *
   * @param array The Array of Objects
   * @param len The Array of Column Widths
   * @return The Fixed Width String
   */
  public static String toFixedString (Object[] array,int[] len) {
    int n,l;
    char ch;
    String str;
    StringBuilder buf = new StringBuilder();
    for(int i = 0;i < array.length;i++) {
      n = 0;
      l = Math.abs(len[i]);
      if((str = String.valueOf(array[i])) != null) {
        if((n = str.length()) > l) {
          buf.append(str.substring(0,l));
        } else {
          if(len[i] < 0) {
            while(n++ < l) {
              buf.append('0');
            }
          }
          buf.append(str);
          while(n++ < l) {
            buf.append(' ');
          }
        }
      } else {
        ch = (len[i] < 0) ? '0' : ' ';
        while(n++ < l) {
          buf.append(ch);
        }
      }
    }
    for(int i = array.length;i < len.length;i++) {
      l = Math.abs(len[i]);
      ch = (len[i] < 0) ? '0' : ' ';
      for(n = 0;n < l;n++) {
        buf.append(ch);
      }
    }
    return buf.toString();
  } //toFixedString

  ////////// Date Conversion //////////

  /**
   * Convert a Date to a Calendar
   *
   * @param date The Date Object
   * @return The equivalent Calendar Object
   */
  public static Calendar toCalendar (Date date) {
    Calendar cal = Calendar.getInstance();
    if(date != null) {
      cal.setTime(date);
    }
    return cal;
  } //toCalendar

  /**
   * Convert a Date String to a Calendar
   *
   * @param date The Date String
   * @return The Calendar Object
   */
  public static Calendar toCalendar (String date) {
    Pattern pattern = Pattern.compile(DATE_REGEX);
    Matcher matcher = pattern.matcher(date);
    if(matcher.find()) {
      int y = 1,m = Calendar.JANUARY,d = 1;
      String data;
      if((data = matcher.group(3)) != null) {
        y = Integer.parseInt(data);
        if(y < 100) {
          if(y < 25) { // Range = 1925 - 2024
            y += 2000;
          } else {
            y += 1900;
          }
        }
      }
      if((data = matcher.group(1)) != null) {
        if(Misc.isNumeric(data)) {
          m = Integer.parseInt(data) - 1;
        } else if(data.length() == 3) {
          m = Misc.arrayIndexOf(Chrono.getMonthAbbreviations(),data);
        } else {
          m = Misc.arrayIndexOf(Chrono.getMonthNames(),data);
        }
      }
      if((data = matcher.group(2)) != null) {
        d = Integer.parseInt(data);
      }
      return new GregorianCalendar(y,m,d);
    } else {
      pattern = Pattern.compile(ISO_REGEX);
      matcher = pattern.matcher(date);
      if(matcher.find()) {
        int y = Integer.parseInt(matcher.group(1));
        int m = Integer.parseInt(matcher.group(2));
        int d = Integer.parseInt(matcher.group(3));
        return new GregorianCalendar(y,m - 1,d);
      }
    }
    return null;
  } //toCalendar

  /**
   * Parse a Date String into a Date Object
   *
   * @param date The Date String
   * @return The Date Object
   */
  public static Date toDate (String date) {
    Calendar cal = toCalendar(date);
    return (cal != null) ? cal.getTime() : null;
  } //toDate

  /**
   * Parse a Date String into a Date Object using a specific Format String
   *
   * @param date The Date String
   * @param format The Date Format String
   * @return The Date Object
   */
  public static Date toDate (String date,String format) {
    if(date != null) {
      if(format != null) {
        try {
          return new SimpleDateFormat(format).parse(date);
        } catch(ParseException x) {
          // That didn't work, try using the unformatted method
        }
      }
      return toDate(date);
    }
    return null;
  } //toDate

  /**
   * Format a Java Date Object as an ISO Date/Time String
   *
   * @param date The Date to Format
   * @return The ISO Date String
   */
  public static String toISOString (Date date) {
    if(date != null) {
      return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date);
    }
    return null;
  } //toISOString

} //*Convert
